using System;
using System.Threading;
using Microsoft.SPOT.Hardware;

namespace DotNetWarrior.NetMF.IO
{
    public class MX25l3206BlockDriver : IBlockDriver
    {
        private readonly OutputPort _accessIndicator;
        private readonly MX25l3206 _flash;

        public MX25l3206BlockDriver(SPI spi, Cpu.Pin accessIndicatorPin, int pagesPerCluster)
        {
            _flash = new MX25l3206(spi);
            ClusterSize = (ushort) (pagesPerCluster*MX25l3206.PageSize);
            _accessIndicator = new OutputPort(accessIndicatorPin, false);
        }

        public void Erase()
        {
            try
            {
                _accessIndicator.Write(true);
                _flash.EraseChip();
            }
            finally
            {
                _accessIndicator.Write(false);
            }
        }

        public void EraseSector(int sectorId)
        {
            try
            {
                _accessIndicator.Write(true);
                _flash.EraseSector(sectorId, 1);
            }
            finally
            {
                _accessIndicator.Write(false);
            }
        }

        public void Read(ushort clusterId, int clusterOffset, byte[] data, int index, int count)
        {
            try
            {
                _accessIndicator.Write(true);
                var address = (clusterId*ClusterSize) + clusterOffset;
                _flash.ReadData(address, data, index, count);
            }
            finally
            {
                _accessIndicator.Write(false);
            }
        }

        public void Write(ushort clusterId, int clusterOffset, byte[] data, int index, int count)
        {
            try
            {
                _accessIndicator.Write(true);
                var address = (clusterId*ClusterSize) + clusterOffset;
                _flash.WriteData(address, data, index, count);
            }
            finally
            {
                _accessIndicator.Write(false);
            }
        }

        public int DeviceSize
        {
            get { return MX25l3206.MaxAddress; }
        }

        public int SectorSize
        {
            get { return MX25l3206.SectorSize; }
        }

        public ushort ClusterSize { get; private set; }
    }

    internal class MX25l3206
    {
        private const byte CMD_GET_IDENTIFICATION = 0x9F;
        private const byte CMD_ERASE_SECTOR = 0x20;
        private const byte CMD_ERASE_BLOCK = 0xD8;
        private const byte CMD_ERASE_CHIP = 0xC7;
        private const byte CMD_WRITE_PAGE = 0x2;
        private const byte CMD_READ = 0x03;
        private const byte CMD_WRITE_ENABLE = 0x6;
        private const byte CMD_READ_STATUS = 0x5;
        public const int MaxAddress = 0x400000;
        public const int PageSize = 256;
        public const int SectorSize = 4*1024;
        public const int BlockSize = 64*1024;
        private const byte ID_MANUFACTURE = 0xC2;
        private const byte ID_DEVICE_0 = 0x20;
        private const byte ID_DEVICE_1 = 0x16;
        private const byte DUMMY_BYTE = 0x00;
        private readonly SPI _spi;
        private readonly byte[] data1;
        private readonly byte[] data2;
        private readonly byte[] data4;
        private readonly byte[] dataPage;

        public MX25l3206(SPI spi)
        {
            _spi = spi;

            data1 = new byte[1];
            data2 = new byte[2];
            data4 = new byte[4];
            dataPage = new byte[PageSize + 4];
        }

        public bool WriteEnable()
        {
            data1[0] = CMD_WRITE_ENABLE;
            _spi.Write(data1);
            data1[0] = CMD_READ_STATUS;
            _spi.WriteRead(data1, data2);
            return ((data2[1] & 0x02) != 0);
        }

        public byte[] GetIdentification()
        {
            data1[0] = CMD_GET_IDENTIFICATION;
            _spi.WriteRead(data1, data4);

            if ((data4[1] == 0xFF && data4[2] == 0xFF && data4[3] == 0xFF) ||
                (data4[1] == 0 && data4[2] == 0 && data4[3] == 0))
            {
                throw new Exception("Can not initialize flash");
            }

            return data4;
        }

        public bool WriteInProgress()
        {
            data1[0] = CMD_READ_STATUS;
            _spi.WriteRead(data1, data2);
            return ((data2[1] & 0x01) != 0);
        }

        public void EraseChip()
        {
            while (WriteEnable() == false) Thread.Sleep(0);
            data1[0] = CMD_ERASE_CHIP;
            _spi.Write(data1);
            while (WriteInProgress()) Thread.Sleep(0);
        }

        public bool EraseBlock(int block, int count)
        {
            if ((block + count)*BlockSize > MaxAddress)
            {
                throw new Exception("Invalid params");
            }

            var address = block*BlockSize;
            var i = 0;
            for (i = 0; i < count; i++)
            {
                while (WriteEnable() == false) Thread.Sleep(0);

                data4[0] = CMD_ERASE_BLOCK;
                data4[1] = (byte) (address >> 16);
                data4[2] = (byte) (address >> 8);
                data4[3] = (byte) (address >> 0);
                _spi.Write(data4);
                address += BlockSize;

                while (WriteInProgress()) Thread.Sleep(0);
            }
            return i == count;
        }

        public bool EraseSector(int sector, int count)
        {
            if ((sector + count)*SectorSize > MaxAddress) throw new ArgumentException("Invalid sector + count");

            var address = sector*SectorSize;
            var i = 0;
            for (i = 0; i < count; i++)
            {
                while (WriteEnable() == false) Thread.Sleep(0);

                data4[0] = CMD_ERASE_SECTOR;
                data4[1] = (byte) (address >> 16);
                data4[2] = (byte) (address >> 8);
                data4[3] = (byte) (address >> 0);
                _spi.Write(data4);
                address += SectorSize;

                while (WriteInProgress()) Thread.Sleep(0);
            }
            return i == count;
        }

        public bool WriteData(int address, byte[] array, int index, int count)
        {
            if ((array.Length - index) < count) throw new ArgumentException("Invalid index + count");
            if ((MaxAddress - count) < address) throw new ArgumentException("Invalid address + count");

            var block = count/PageSize;
            var length = count;
            var i = 0;
            if (block > 0)
            {
                for (i = 0; i < block; i++)
                {
                    while (WriteEnable() == false) Thread.Sleep(0);

                    dataPage[0] = CMD_WRITE_PAGE;
                    dataPage[1] = (byte) (address >> 16);
                    dataPage[2] = (byte) (address >> 8);
                    dataPage[3] = (byte) (address >> 0);
                    Array.Copy(array, index + (i*PageSize), dataPage, 4, PageSize);
                    _spi.Write(dataPage);

                    while (WriteInProgress()) Thread.Sleep(0);
                    address += PageSize;
                    length -= PageSize;
                }
            }

            if (length > 0)
            {
                while (WriteEnable() == false) Thread.Sleep(0);

                dataPage[0] = CMD_WRITE_PAGE;
                dataPage[1] = (byte) (address >> 16);
                dataPage[2] = (byte) (address >> 8);
                dataPage[3] = (byte) (address >> 0);
                Array.Copy(array, index + (i*PageSize), dataPage, 4, length);
                _spi.WriteRead(dataPage, 0, length + 4, null, 0, 0, 0);

                while (WriteInProgress()) Thread.Sleep(0);
                address += length;
                length -= length;
            }

            return length == 0;
        }

        public void ReadData(int address, byte[] array, int index, int count)
        {
            if ((array.Length - index) < count) throw new ArgumentException("Invalid index + count");
            if ((MaxAddress - count) < address) throw new ArgumentException("Invalid address + count");

            while (WriteEnable() == false) Thread.Sleep(0);

            data4[0] = CMD_READ;
            data4[1] = (byte) (address >> 16);
            data4[2] = (byte) (address >> 8);
            data4[3] = (byte) (address >> 0);
            _spi.WriteRead(data4, 0, 4, array, index, count, 4);
        }
    }
}